<?php
/*****************************************
 This file is part of the Dynamo Core CMS
 Copyright (c) Dynamo Studios 2008

 *** NOTE ***
 Permission is hereby granted to execute this file as a
 web service under the terms of the license agreement.

 See LICENSE.txt, in the system folder.

 All other rights, other than those expressed in the license
 document, are reserved.

 ****************************************/

/**
 * @name Object Services
 * @version 0.3.2 (Alvin)
 * @abstract
 * Provides base classes and interfaces for CMS objects and
 * object collections.
 *
 * @author Sean Micklethwaite
 */

include_once("stdcls.inc");


interface IDycmsObject
{
	function GetSummary();
	function GetId();
	function IsCollection();

	function Edit(IDycmsClient &$r_client);
	function Render(cDycmsClientNode &$r_client);

	function Count();
}

abstract class cDycmsAbstractCollection implements IDycmsObject
{
	protected $mzObject;
	protected $mzCriteria;
	protected $msCatIndex;
	protected $mzCatValue;
	protected $mzResultSet;
	protected $mzClass;
	protected $miCount;

	function & __construct($vzz_set, $vz_criteria = array(), $argv = array())
	{
		//
		// Check for empty set
		//

		if(!is_array($vzz_set) || count($vzz_set) == 0)
			$this->miCount = 0;
		else{
			$this->mzCriteria = $vz_criteria;
			$this->mzResultSet = $vzz_set;
			
			if($argv['noCat']){
				
				//
				// No categorisation - just load all objects
				//
				
				$this->miCount = 0;
				foreach ($vzz_set as $vz_result)
				{
					if(!isset($this->mzClass[$vz_result['$cnum']]))
						$this->mzClass[$vz_result['$cnum']]
							= dyLoadClass($vz_result['$cnum']);
					
					$this->mzObject[$this->miCount++]
						= $this->mzClass[$vz_result['$cnum']]
							->LoadObject($vz_result['$id']);
				}
				return;
			}
			
			if($argv['objectQuery'] && !$argv['partialSet']) {
				
				//
				// This is a one class object query. Load all
				// objects, passing the attributes as an array.
				//
				// TODO: security checks on these.
				//
				
				$this->mzObject = array();
				$this->mzClass[0] = dyLoadClassById($argv['classId']);
				$this->miCount = 0;
				foreach($vzz_set as $vzObj){
					$this->mzObject[$this->miCount++] =
						$this->mzClass[0]->LoadObjectByArray($vzObj);
				}
				
				return;
			}
			
			
			if(!isset($argv['catIndex'])) {
				//
				// Create the category index
				//

				foreach($vzz_set[0] as $s_index => $val)
				{
					if($s_index[0] == '$')
						continue;

					//
					// Get all unique values
					//

					$this->mzCatValue = $this->GetIndexValues($s_index);


					//
					// Stop if we have more than one unique value
					//

					if(count($this->mzCatValue) > 1)
					{
						$this->msCatIndex = $s_index;
						$this->miCount = count($this->mzCatValue);
						return;
					}
				}
			}else{
				/** Categorise by specified cat index **/
				$this->mzCatValue = $this->GetIndexValues($argv['catIndex']);
				$this->msCatIndex = $argv['catIndex'];
				$this->miCount = count($this->mzCatValue);
			};
			

			//
			// Cannot categorize by index - load all classes
			//

			foreach($vzz_set as $vz_result)
			{
				if(!isset($this->mzClass[$vz_result['$cnum']]))
				{
					$this->mzClass[$vz_result['$cnum']]
						= dyLoadClass($vz_result['$cnum']);
				}
			}

			if(count($this->mzClass) == 1)
			{
				//
				// Only one class - load all objects
				//

				foreach ($vzz_set as $vz_result)
				{
					$this->mzObject[$vz_result['$id']]
						= $this->mzClass[$vz_result['$cnum']]
							->LoadObject($vz_result['$id']);
				}

				$this->miCount = count($this->mzObject);
			}else{
				$this->miCount = count($this->mzClass);
			}
		}
	}

	function GetIndexValues($s_index){
		$z = array();

		foreach ($this->mzResultSet as $vz_result){
			if(array_search($vz_result[$s_index], $z) === false)
				array_push($z, $vz_result[$s_index]);
		}

		return $z;
	}

	function GetFilteredIndexValues($s_index, $argv = array(), $assoc = null){
		$z = array();

		foreach ($this->mzResultSet as $vz_result){
			//
			// Skip over values which do not match our criteria
			//

			$ok = true;

			foreach($argv as $idx => $val) {
				if($vz_result[$idx] != $val) {
					$ok = false;
					break;
				};
			};

			if(!$ok) continue;


			//
			// Criteria satisfied - insert into set
			//

			if($assoc) {
				if(!isset($z[$vz_result[$s_index]]))
					$z[$vz_result[$s_index]] = $vz_result[$assoc];
			} elseif(array_search($vz_result[$s_index], $z) === false)
				array_push($z, $vz_result[$s_index]);
		}

		return $z;
	}

	function IsCollection(){
		return true;
	}

	public function Render(cDycmsClientNode &$r_client, $i = -1)
	{
		global $gViewerUrl, $gEditorUrl;

		if($i >= 0)
			return $this->mzObject[$i]->Render($r_client);
		else if($this->msCatIndex)
		{
			//
			// Collection is categorized
			//

			$r_node = new cUiCollection($this->miCount.
					" possible values for '".$this->msCatIndex."'");

			$vz_criteria = $this->mzCriteria;

			foreach ($this->mzCatValue as $s_value)
			{
				$vz_criteria[$this->msCatIndex] = $s_value;

				$r_node->AddChild(new cUiCollectionSubCollection
						($s_value, $vz_criteria));
			}
		}
		else if(count($this->mzClass) > 1)
		{
			// TODO
		}
		else
		{
			$r_node = new cUiCollection("Query returned ".
				$this->miCount . " results");

			foreach ($this->mzObject as $obj)
			{
				$r_node->AddChild(new cUiCollectionObject($obj));
			}
		}

		$r_client->AddChild($r_node);
	}

	public function DisplayXml()
	{
		header("Content-Type: text/xml", true);

		$count = count($this->mObject);
		$count > 1 ? print("<collection>") : print('');

		foreach($this->mzObject as $obj)
		{
			$obj->DisplayXml();
		};

		$count > 1 ? print("</collection>") : print('');
	}

	public function Display()
	{
		$ret = $this->GetDisplay();
		uiTryError($ret);
		echo $ret;
	}

	public function AddObject(IDycmsObject $obj)
	{
		if($obj->CheckPriv(S_LIST))
			array_push($this->mzObject, $obj);
	}

	function GetUniqueValues($attrib)
	{
		$ar = array();
		foreach($this->mzObject as $obj)
		{
			$ar[$obj->Get($attrib)] = true;
		};

		return array_keys($ar);
	}

	function __get($name)
	{
		if(count($this->mzObject) == 1)
		{
			return $this->mzObject[0]->Get($name);
		}
		else
			return NULL;
	}

	function Get($name, $idx = 0)
	{
		return $this->mzObject[$idx]->Get($name);
	}

	function GetId($i = 0)
	{
		if($this->mzObject[$i]->CheckPriv(S_LIST))
			return $this->mzObject[$i]->GetId();
	}

	function Count()
	{
		return $this->miCount;
	}

	function GetObjectId($idx)
	{
		if($this->mzObject[$idx]->CheckPriv(S_LIST))
			return $this->mzObject[$idx]->GetId();
	}
	
	function & GetOwner($idx){
		if($this->mzObject[$idx]->CheckPriv(S_LIST))
			return $this->mzObject[$idx]->GetOwner();
	}

	function Edit(IDycmsClient &$r_client)
	{
		if(count($this->mzObject) == 1)
		{
			return $this->mzObject[0]->Edit();
		}
		else
			return ENOTFOUND;
	}
}

class cDycmsCollection extends cDycmsAbstractCollection
{
	public function GetSummary($i = -1)
	{
		$count = count($this->mzObject);
		if($this->mCatValue)
			return $this->mCatValue;
		else if($count == 1)
			return $this->mzObject[0]->GetSummary();
		else if($i != -1)
			return $this->mzObject[$i]->GetSummary();
		else
			return "Query matched $count objects";
	}
}

class cDycmsAttributes
{
	protected $mValue;

	public function __construct()
	{
		$this->mValue = array();
	}

	public function Add($name, $datatype, $len = 0, $index = false)
	{
		$this->mValue[$name] = array("datatype"=>$datatype, "index"=>$index, "len"=>$len);
	}

	public function Count()
	{
		return count($this->mValue);
	}

	public function Dump()
	{
		foreach ($this->mValue as $attrib => $info)
		{
			echo "$attrib: ";
			foreach ($info as $key => $val)
			{
				echo "$key=$val ";
			};
			echo "<br/>";
		};
	}

	public function IsAttribute($name, $datatype)
	{
		foreach ($this->mValue as $attrib => $info)
		{
			if($attrib != $name)
				return false;
			if($info['datatype'] == "int")
			{
				if($datatype != "int")
					return false;
			};
		};
		return true;
	}

	public function CheckCompatible($vzAttrib)
	{
		foreach ($vzAttrib as $name => $val)
		{
			if(ctype_digit($val))
				$dt = "int";
			else
				$dt = "string";
			if(!$this->IsAttribute($name, $dt))
				return false;
		};
		return true;
	}
}

abstract class cDycmsObject extends cDbObject implements IDycmsObject
{
	protected	$mToken;
	protected 	$mOwner;
	protected 	$mClass;
	protected 	$mzCoreAttributes;
	static protected	$mSummaryValue;

	abstract function	LoadObject();

	final function Count()
	{
		return 1;
	}

	function GetUrl(){
		return dyViewerUrl($this->mClsId, $this->mId);
	}
	
	function StartEdit(IDycmsClient &$r_client)
	{
		$editor = null;

		// Import editor widgets
		uiLoadLibrary('editor');
		
		switch($_POST['$$os-editorAction'])
		{
		 	case 'view':
		 	header('Location: '.$this->GetUrl().'&$$ui-ajaxOverride='.urlencode($this->GetUrl()));
		 	break;
			
			case 'preview':
			
			//
			// Update this copy of the object with the new details, but don't save.
			// Then call render on the object, and set the result container to new.
			// If we are running without AJAX, display the editor too.
			//
			
		 	$this->DoEdit($_POST);
			$this->LoadObject();
			$this->Render($r_client);
			
			if(dyPostOrGet('$$ui-resultContainer') == null)
				$editor = new cUiEditor($this);
			
		 	break;
		 	
			case "forceDelete":

			//
			// Security check
			//

			$this->Validate(S_ADMINISTRATE);

			if(dbDeleteObject($this->mClsId, $this->mId))
			{
				$this->mId = 0;
				$this->LoadObject(NULL);

				$editor = new cUiEditor($this);
				$editor->AddChild(new cUiEditorMsg(uiGetIcon("accept").
					"Object deleted"));
			}
			else
				throw new dyExDbError("Failed to delete object");

			break;


			case "delete":

			$icon = uiGetIcon("exclamation");
			$del = uiGetIcon('delete').' Delete';
			$save = uiGetIcon('disk').' Keep';

			//
			// Create confirmation form
			//

			$r_client->ContentNode()->AddChild(
			new cHtmlCode(<<<HTMLEND
<form action="{$_SERVER['REQUEST_URI']}" method="POST">
<h3>$icon Warning: this action is irreversible.</h3>
<p>Are you sure you want to delete '{$this->GetSummary()}'?</p>
<button type="submit" name="\$\$os-editorAction" value="forceDelete">$del</button>
<button type="submit" name="\$\$os-editorAction" value="edit">$save</button>
</form>
HTMLEND
			));

			break;

			case 'save':

			//
			// Apply changes
			//
			
			$this->DoEdit($_POST);
			$this->Write();
			
			header('Location: '.dyEditorUrl($this->mClsId, $this->mId,
			array('$$os-editorStatus'=>'saved',
					'$$ui-resultContainer'=>$_GET['$$ui-resultContainer'])));

			break;


			case 'saveNew':

			//
			// Apply changes
			//

			$this->mId = 0;
			$this->DoEdit($_POST);
			$this->Write();
			
			header('Location: '.dyEditorUrl($this->mClsId, $this->mId,
				array('$$os-editorStatus' => 'savedNew',
					'$$ui-resultContainer'=>$_GET['$$ui-resultContainer'])));

			break;


			case 'doPub':

			//
			// Apply changes to core attributes
			// TODO: check capability
			//

			$this->Validate(S_MODIFY);
			dyRequirePriv($this->mClass->GetPublishPriv());
			
			if($this->DoPublish($_POST))
			{
				header('Location: '.dyEditorUrl($this->mClsId, $this->mId,
					array('$$os-editorStatus'=>'published',
					'$$ui-resultContainer'=>$_GET['$$ui-resultContainer'])));
			};

			break;


			case 'pub':

			//
			// Apply changes
			//

			$this->DoEdit($_POST);
			$this->Write();

			header('Location: '.dyEditorUrl($this->mClsId, $this->mId,
				array('$$os-editorAction'=>'pub','$$os-editorStatus'=>'saved',
					'$$ui-resultContainer'=>$_GET['$$ui-resultContainer'])));

			break;


			case 'pubNew':

			//
			// Apply changes
			//

			$this->mId = 0;
			$this->DoEdit($_POST);
			$this->Write();
			
			header('Location: '.dyEditorUrl($this->mClsId, $this->mId,
				array('$$os-editorAction'=>'pub','$$os-editorStatus'=>'savedNew',
					'$$ui-resultContainer'=>$_GET['$$ui-resultContainer'])));

			break;


			case 'edit':

			header('Location: '.dyEditorUrl($this->mClsId, $this->mId, true));
			break;


			default:

			if($_GET['$$os-editorAction'] == 'pub')
			{
				//
				// Sanity checks - object must exist, user
				// must be able to see it AND publish objects of
				// this class
				//
				
				if($this->mId == 0)
					throw new dyExInvalidOp("Cannot publish non-existent object");
				
				$this->Validate(S_MODIFY);
				dyRequirePriv($this->mClass->GetPublishPriv());
				
				//
				// Create publisher form
				//

				$editor = new cUiPublisher($this);

				//
				// Load core attributes
				//

				$this->mzCoreAttributes = $this->mClass->GetIndexValues($this->mId);
			}
			else
			{
				// Get Changes
				$this->DoEdit($_POST);
				
				//
				// Create editor form
				//

				$editor = new cUiEditor($this);
			};

			if(isset($_GET['$$os-editorStatus'])) {
				switch($_GET['$$os-editorStatus']) {
					case 'savedNew':
					$editor->AddChild(new cUiEditorMsg(uiGetIcon('accept').
						" New object created successfully"));
					break;

					case 'saved':
					$editor->AddChild(new cUiEditorMsg(uiGetIcon('accept').
						" Changes saved"));
					break;

					case 'published':
					$editor->AddChild(new cUiEditorMsg(uiGetIcon('accept').
							" Content published successfully"));
					break;
				};
			};
		}; // switch

		return $editor;
	}

	function DrawEditor(cUiEditor &$editor)
	{
		//
		// Draw the default editor form
		//

		foreach($this->mAttributes as $name => $val)
		{
			//
			// Create a text area for each attribute, skipping internal attributes
			//

			if($name[0] != '$')
				$editor->AddChild(new cUiEditorTextArea($name, $val));
		};
		
		$editor->AddDefaultButtons();
	}

	function DrawPublisher(cUiPublisher &$r_pub)
	{
		//
		// Draw the default publisher form
		//
		
		foreach($this->mzCoreAttributes as $name => $val)
		{

			$r_pub->AddChild(new cUiTextBox(array(
				'name'=>$name, 'value'=>$val
			), $name));
		};
		
		$r_pub->AddDefaultButtons();
	}

	function Edit(IDycmsClient &$r_client)
	{
		try
		{
			$editor = $this->StartEdit($r_client);
			
			$r_client->AddMetaData('objectId', $this->mId);
			$r_client->AddMetaData('objectSummary', $this->GetSummary());
			
			if($editor instanceof cUiEditor)
			{
				$this->DrawEditor($editor);
				$r_client->ContentNode()->AddChild($editor);
			}
			else if($editor instanceof cUiPublisher)
			{
				$this->DrawPublisher($editor);
				$r_client->ContentNode()->AddChild($editor);
			}
			else
			{
				$r_client->AddMetaData('resultContainer', '');
			}
		}
		catch (Exception $e)
		{
			$r_client->ContentNode()->AddChild(new cUiException($e));
		};
	}

	function DoEdit($vzAttrib)
	{
		$c = 0;
		
		if($this->mId == 0)
			dyRequirePriv($this->mClass->GetCreatePriv());
		else
			$this->Validate(S_MODIFY);
		
		foreach ($vzAttrib as $name => $val)
		{
			if($name[0] != '$')
			{
				$this->mAttributes[$name] = $val;
				$c++;
			}
		};
		return $c;
	}
	
	function Write(){
		$this->mAttributes['$token'] = $this->mToken;
        $this->mAttributes['$owner'] = $this->mOwner->GetUserNum();
        return $this->DoWrite();
	}

	function DoPublish($vz_attrib)
	{
		foreach ($vz_attrib as $name => $val)
		{
			if($name[0] != '$')
				$this->mzCoreAttributes[$name] = $val;
		}

		$this->mClass->SetIndexValues($this->mId, $this->mzCoreAttributes);
		return true;
	}

	function Render(cDycmsClientNode &$r_client){
	/**
	* @desc 
	* Performs security validation before calling DoRender() to render
	* the object.
	* 
	* @param $r_client	Reference to the page to render to
	*/
		try
		{
			$this->Validate(S_DISPLAY);
			$this->DoRender($r_client);
		} catch(Exception $e) {
			$r_client->AddChild(new cUiException($e));
		}
	}

    function TakeOwnership(){
    /**
     * Transfers ownership of this object to the currently logged in
     * user. The user must have strictly greater privileges than the
     * current owner and must satisfy 'special' privileges for the object.
     *
     * @note	The 'ownall' capability mitigates the 2nd privilege requirement.
     **/
        global $gClient;

        if($this->mId == 0)
            throw new dyExInvalidOp('Cannot take ownership - object does not exist');

        if($gClient->IsValidated()){
            if($gClient->GetUserNum() == $this->mOwner->GetUserNum())
                return true;

            if(($gClient->GetPrivs() > $this->mOwner->GetPrivs())
                && ($this->CheckPriv(SPECIAL)
                || $gClient->CheckCapability('ownall'))){

                //
                // Set new owner
                //

                $this->mOwner = $gClient;
                return $this->Write();
            } else
                throw new dyExAuth(
                'Insufficient privileges');
        } else
            throw new dyExDenied(
                'You must be logged in');
    }
    
    function GrantOwnership($uNum){
    /**
    * @desc 
    * Transfers ownership of the object to the user corresponding to the
    * passed user number. If the logged in user does not own the object,
    * we will attempt to take ownership first.
    * 
    * @param $uNum	User Number to grant ownership to
    */
    	global $gClient;
    	
    	if($this->mId == 0)
    		throw new dyExInvalidOp('Cannot grant ownership - object does not exist');
    	
    	if($gClient->GetUserNum() !== $this->mOwner->GetUserNum())
    		$this->TakeOwnership();
    	
    	//
    	// Validate the passed number
    	//
    	
    	$cl =& dyLoadClient($uNum);
    	if($cl->GetUserNum() != $uNum)
    		throw new dyExValidation("Invalid user number: $uNum != ".$cl->GetUserNum());
    	
    	
    	//
    	// Set new owner
    	//
    	
    	$this->mOwner = $cl;
    	return $this->Write();
	}

    function SetToken($tok){
    /**
    * @desc 
    * Sets a new security token for this object. Logged in user must either
    * be the object owner, or satisfy SPECIAL privileges for the object.
    * 
    * @param $tok	New security token
    **/
        if($this->mId == 0)
            throw new dyExInvalidOp('Cannot set token - object does not exist');

        $this->Validate(SPECIAL);

        //
        // Set new token
        //

        $this->mToken = $this->mAttributes['$token'] = $tok;
        return $this->Write();
    }

	function DoRender(cDycmsClientNode &$r_client) {
        $r_client->AddChild(
            uiGetIcon('exclamation').
            $this->mClass->GetName().
            's cannot be displayed (method '.
            $this->mClsId.'::DoRender() undefined)');
	}

	function Validate($access)
	{
		return dyValidateToken($access, $this);
	}

	function CheckPriv($access)
	{
		return dyCheckToken($access, $this);
	}

	function GetOwnerNum(){
		return $this->mOwner->GetUserNum();
	}
	function & GetOwner(){
		return $this->mOwner;
	}
    function GetToken(){
        return $this->mToken;
    }
    function GetTokenType(){
        return $this->mClass->GetTokenType($this->mToken);
    }

	function IsCollection()
	{
		return false;
	}

    function Get($attr){
        if($this->CheckPriv(READ))
            return $this->mAttributes[$attr];
        else
            return false;
    }

	function __construct($id = 0)
	{
        global $gClient;
		$this->mClass = dyLoadClassById($this->mClsId);

		if($id == 0){
            $this->mId = 0;
            $this->mOwner = $gClient;
            $this->mToken = $this->mClass->GetPublicToken();
            $this->Create();
			return;
        }else if(is_array($id)){
            //
            // Load from attribute array
            //

            $this->mId = $id['$id'];
            $this->mAttributes = $id;
		}else{
			//
			// Existing object - load it
			//

            $this->mId = $id;
			$this->Read();
		};

        $this->LoadObject();
		$this->mToken = (float)$this->mAttributes['$token'];
		$this->mOwner =& dyLoadClient($this->mAttributes['$owner']);
	}

	function	GetId(){
		return $this->mId;
	}
	function	Exists(){
		return ($this->mId > 0);
	}
	function	GetClsId(){
		return $this->mClsId;
	}
	function	GetClassNum(){
		return $this->mClass->GetClassNum();
	}
	function & GetClass(){
		return $this->mClass;
	}

    function    Create(){
        throw new dyExInvalidOp('Cannot create new '.
            $this->mClass->GetName().'s');
    }
}

?>
